Interactive Mapping of the Scottish Parliament Constituency Results.

politics
mapview
ggpol
holyrood

A quick tutorial on how to make an interactive constituency result map using the Scottish Parliament Constituency Results from 2016. I will update this page with the 2021 results when they are released.

Thomas Zwagerman https://twitter.com/thomzwa (Centre for Ecology & Hydrology)https://www.ceh.ac.uk/
2022-02-14
Show code
knitr::include_graphics("preview.jpg")

This will be a quick introduction to making interactive election result maps in R. I’ll be using the mapview package to do most of the heavy lifting. Click on each show code section to show the relevant annotated code on how I did each step. Copy anything you like - you can also find the full source code on my github page (link at the top of this page).

Downloading the data

I downloaded the 2016 election results from The Electoral Commission. The constituency shapefile can be found on the ONS Website.

Show code
# Libraries----
library(sf)
library(readxl)
library(dplyr)
library(tidyr)
library(mapview)
library(leaflet)
library(leafpop)
library(ggplot2)
library(knitr)
library(scales)

# Read in the files=----
constituency_shp <- st_read("Scottish_Parliamentary_Constituencies_(May_2016)_Boundaries.shp")
results_df <- read_excel("Electoral-Data-Results-May-2016-Scottish-Parliament-elections.xls",
  sheet = "Constituencies - Results",
  range = "A2:AA75"
)

Cleaning the dataset

On our map we’re planning to show the colour of each party that won the constituency, and then when clicking on the map we want to show the breakdown in voting share on a bar chart.

To make that happen I’m splitting the winner and the vote share for each constituency into seperate dataframes.

Show code
# Remove some of the turnout information, not needed
results_df <- as.data.frame(results_df) %>%
  select(-4:-17)

# Create a dataframe for the constituency winner----
winner_df <- results_df %>%
  select(`ONS Code`, Constituency, Region, Win)

# Create a dataframe for the vote share by constituency
voteshare_df <- results_df %>%
  select(-Win, -Second)
voteshare_df <- pivot_longer(voteshare_df,
  cols = c(4:11),
  names_to = "party",
  values_to = "share"
)

# Take first three letters of each party only
voteshare_df$party <- substr(voteshare_df$party, 1, 3)

This is what the “winner” dataset looks like:

Show code
kable(head(winner_df))
ONS Code Constituency Region Win
S16000074 Aberdeen Central North East Scotland SNP
S16000075 Aberdeen Donside North East Scotland SNP
S16000076 Aberdeen S. and N. Kincardine North East Scotland SNP
S16000077 Aberdeenshire East North East Scotland SNP
S16000078 Aberdeenshire West North East Scotland Con
S16000079 Airdrie & Shotts Central SNP

And this is the what the dataset of the vote share looks like:

Show code
kable(head(voteshare_df))
ONS Code Constituency Region party share
S16000074 Aberdeen Central North East Scotland CON 22.55
S16000074 Aberdeen Central North East Scotland LAB 27.33
S16000074 Aberdeen Central North East Scotland LIB 6.50
S16000074 Aberdeen Central North East Scotland SNP 43.62
S16000074 Aberdeen Central North East Scotland IND 0.00
S16000074 Aberdeen Central North East Scotland TUS 0.00

I’m also creating a dataframe with colour codes associated with each party (these can be found on a very useful wikipedia index). When I join the colour codes to our shapefile later on, it means I don’t have to manually set any colours!

Show code
# Create a colour code df based on the unique parties
party_colours <- as.data.frame(unique(voteshare_df$party))
colnames(party_colours)[1] <- "party"
party_colours$colour_code <- c(
  # Con
  "#0087DC",
  # Lab
  "#E4003B",
  # Lib
  "#FAA61A",
  # SNP
  "#FDF38E",
  # Independent - just went with a gray colour here
  "#696969",
  # TUSC,
  "#EC008C",
  # Greens
  "#00B140",
  # Other, another generic gray!
  "#808080"
)
kable(head(party_colours))
party colour_code
CON #0087DC
LAB #E4003B
LIB #FAA61A
SNP #FDF38E
IND #696969
TUS #EC008C

Visualising constituency results

In the next section I’m creating a list of ggplot objects. Basically, I’m making a bar chart for each constituency, which can then be linked on our map. Important for this method to work, is that the order in which the list is created matches up with the order of constituencies in our shapefile.

Below is an example of just one of the bar charts, the result from Edinburgh Southern.

Show code
# This might not be needed for the 2021 result, but for some reason
# There is an ONS code discrepancy between the results and our constituency shapefile.
constituency_shp$spc16cd[which(constituency_shp$spc16cd == "S16000147")] <- voteshare_df$`ONS Code`[which(voteshare_df$Constituency == "Glasgow Provan")][1]
constituency_shp$spc16cd[which(constituency_shp$spc16cd == "S16000148")] <- voteshare_df$`ONS Code`[which(voteshare_df$Constituency == "Strathkelvin & Bearsden")][1]

voteshare_df <- voteshare_df[order(voteshare_df$`ONS Code`), ]
rownames(voteshare_df) <- 1:nrow(voteshare_df)
# Hopefully I won't have to repeat that step for the 2021 data!

# Start of our list creation
bar_plot_list <- lapply(unique(voteshare_df$`ONS Code`), function(i) {
  regio_df <- voteshare_df %>%
    filter(`ONS Code` == i) %>%
    dplyr::ungroup() %>%
    select(party, share, Constituency) %>%
    filter(share > 0.1) %>%
    as.data.frame()
  
  regio_df <- regio_df[order(regio_df$share, decreasing = T), ]
  regio_df$share <- round(regio_df$share, 1)
  regio_df <- left_join(regio_df, party_colours)

  # Now making the bar chart!
  ggplot(regio_df) +
    geom_bar(aes(x = party, y = share, fill = party),
      stat = "identity",
      colour = "black", width = 1
    ) +
    # Adding the vote share as text
    geom_text(aes(x = party, y = share, label = paste0(share, "%")), vjust = -0.5, size = 3.5) +
    labs(
      x = NULL, y = NULL, fill = NULL,
      title = paste(unique(regio_df$Constituency))
    ) +
    theme_classic() +
    # Making sure it's ordered from biggest to smallest party
    scale_x_discrete(limits = regio_df$party) +
    # Making sure the colours are automatically matched to the corresponding party
    scale_fill_manual(
      values = regio_df$colour_code,
      limits = regio_df$party
    ) +
    theme(
      axis.line = element_blank(),
      axis.text = element_blank(),
      axis.ticks = element_blank(),
      plot.title = element_text(hjust = 0.5, color = "black"),
      legend.position = "bottom"
    )
})

# Creating the edinburgh south example using the same method as above
edi_south <- voteshare_df %>%
  filter(`ONS Code` == "S16000108") %>%
  dplyr::ungroup() %>%
  select(party, share, Constituency) %>%
  filter(share > 0.1) %>%
  as.data.frame()

edi_south <- left_join(edi_south, party_colours)
edi_south$share <- round(edi_south$share, 1)
edi_south <- edi_south[order(edi_south$share, decreasing = T), ]

ggplot(edi_south) +
  geom_bar(aes(x = party, y = share, fill = party),
    stat = "identity",
    colour = "black", width = 1
  ) +
  geom_text(aes(x = party, y = share, label = paste0(share, "%")), vjust = -0.5, size = 3.5) +
  labs(
    x = NULL, y = NULL, fill = NULL,
    title = paste("Constituency result", unique(edi_south$Constituency))
  ) +
  theme_classic() +
  scale_x_discrete(limits = edi_south$party) +
  scale_fill_manual(
    values = edi_south$colour_code,
    limits = edi_south$party
  ) +
  theme(
    axis.line = element_blank(),
    axis.text = element_blank(),
    axis.ticks = element_blank(),
    plot.title = element_text(hjust = 0.5, color = "black"),
    legend.position = "bottom"
  )

Creating an interactive map

Now it’s time for our map! With our previous work and the mapview package, this is a surprisingly concise bit of code. As mentioned previously, all we have to do is link our winner dataset with our spatial consituencies by ONS code. Then we make sure the order is the same as our list of bar charts, and then put everything into the mapview function.

Clicking on each constituency will show a popup of the bar charts we made above.

Show code
# Joining the constiuency shapefile with the winner dataframe
constituency_shp_mapping <- left_join(constituency_shp, winner_df, by = c("spc16cd" = "ONS Code"))

# The shapefile and results excel sheet had slightly different naming conventions
# For the Liberal Democrats so we have to manually change these a little.
# Make it all caps
constituency_shp_mapping$Win <- toupper(constituency_shp_mapping$Win)
# Swap LD to LIB
constituency_shp_mapping$Win <- gsub("LD", "LIB", constituency_shp_mapping$Win)

# Now join with the colour codes
constituency_shp_mapping <- left_join(constituency_shp_mapping,
  party_colours,
  by = c("Win" = "party")
)

constituency_shp_mapping <- constituency_shp_mapping[order(constituency_shp_mapping$spc16cd), ]
rownames(constituency_shp_mapping) <- 1:nrow(constituency_shp_mapping)

# Calling our mapview function on the constituency_shp_mapping object
mapview(constituency_shp_mapping,
  # Specifying which column's information we want to show
  zcol = "Win",
  # And specify the colours, we can just select the relevant column
  col.regions = constituency_shp_mapping$colour_code,
  # And call on our barchart list to pop up
  popup = popupGraph(bar_plot_list, width = 300, height = 300),
  legend = TRUE,
  layer.name = "Party"
)

And there we go! A very quick way of creating an interactive election map for the Scottish Parliament’s constituencies in R.

Of course, don’t forget that there are also regional seats in the Scottish election under the AMS system - so this isn’t the full picture.